Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Reflection
Imports Co9320.NamaAlert.Model

Namespace ViewModel

    ''' <summary>
    ''' Base class for all ViewModel classes in the application.
    ''' It provides support for property change notifications 
    ''' and has a DisplayName property.  This class is abstract.
    ''' </summary>
    Public MustInherit Class ViewModelBase
        Implements INotifyPropertyChanged, IDisposable, ISerializable


#Region "Constructor"

        Protected Sub New()
        End Sub

#End Region ' Constructor

#Region "DisplayName"

        ''' <summary>
        ''' Returns the user-friendly name of this object.
        ''' Child classes can set this property to a new value,
        ''' or override it to determine the value on-demand.
        ''' </summary>
        Private privateDisplayName As String
        Public Overridable Property DisplayName() As String
            Get
                Return privateDisplayName
            End Get
            Protected Set(ByVal value As String)
                privateDisplayName = value
            End Set
        End Property

#End Region ' DisplayName

#Region "Debugging Aides"

        ''' <summary>
        ''' Warns the developer if this object does not have
        ''' a public property with the specified name. This 
        ''' method does not exist in a Release build.
        ''' </summary>
        <Conditional("DEBUG"), DebuggerStepThrough()> _
        Public Sub VerifyPropertyName(ByVal propertyName As String)
            ' Verify that the property name matches a real,  
            ' public, instance property on this object.
            If TypeDescriptor.GetProperties(Me)(propertyName) Is Nothing Then
                Dim msg As String = "Invalid property name: " & propertyName

                If Me.ThrowOnInvalidPropertyName Then
                    Throw New ArgumentException(msg)
                Else
                    Debug.Fail(msg)
                End If
            End If
        End Sub

        ''' <summary>
        ''' Returns whether an exception is thrown, or if a Debug.Fail() is used
        ''' when an invalid property name is passed to the VerifyPropertyName method.
        ''' The default value is false, but subclasses used by unit tests might 
        ''' override this property's getter to return true.
        ''' </summary>
        Private privateThrowOnInvalidPropertyName As Boolean
        Protected Overridable Property ThrowOnInvalidPropertyName() As Boolean
            Get
                Return privateThrowOnInvalidPropertyName
            End Get
            Set(ByVal value As Boolean)
                privateThrowOnInvalidPropertyName = value
            End Set
        End Property

#End Region ' Debugging Aides

#Region "INotifyPropertyChanged Members"

        ''' <summary>
        ''' Raised when a property on this object has a new value.
        ''' </summary>
        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

        ''' <summary>
        ''' Raises this object's PropertyChanged event.
        ''' </summary>
        ''' <param name="propertyName">The property that has a new value.</param>
        Protected Overridable Sub OnPropertyChanged(ByVal propertyName As String)
            Me.VerifyPropertyName(propertyName)

            Dim handler As PropertyChangedEventHandler = Me.PropertyChangedEvent
            If handler IsNot Nothing Then
                Dim e = New PropertyChangedEventArgs(propertyName)
                handler(Me, e)
            End If
        End Sub

#End Region ' INotifyPropertyChanged Members

#Region "IDisposable Members"

        ''' <summary>
        ''' Invoked when this object is being removed from the application
        ''' and will be subject to garbage collection.
        ''' </summary>
        Public Sub Dispose() Implements IDisposable.Dispose
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub

        ''' <summary>
        ''' Child classes can override this method to perform 
        ''' clean-up logic, such as removing event handlers.
        ''' </summary>
        Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        End Sub

#End Region ' IDisposable Members

#Region "ISerializable"

        Public Function Serialize() As System.Xml.Linq.XElement Implements ISerializable.Serialize
            Return Serialize(Me.GetType.Name)
        End Function

        Public Overridable Function Serialize(ByVal name As String) As System.Xml.Linq.XElement Implements ISerializable.Serialize
            If name = "" Then
                name = Me.GetType.Name
            End If

            Dim rootElement = New XElement(name)

            For Each propertyInfo In Me.GetType.GetProperties()

                If Not propertyInfo.CanWrite AndAlso _
                   propertyInfo.PropertyType.IsGenericType AndAlso _
                   propertyInfo.PropertyType.FullName.StartsWith("System.Collections.ObjectModel.ObservableCollection", StringComparison.Ordinal) Then
                    ' ReadOnly ObservableCollection Property

                    Dim collectionElement = New XElement(propertyInfo.Name)

                    For Each item In DirectCast(propertyInfo.GetValue(Me, Nothing), IList)
                        If item.GetType.IsClass AndAlso _
                           Not item.GetType Is GetType(String) Then

                            If item.GetType.BaseType.GetInterface("ISerializable") IsNot Nothing Then
                                Dim elements = DirectCast(item.GetType.InvokeMember("Serialize", BindingFlags.InvokeMethod, Nothing, item, New Object() {""}, System.Globalization.CultureInfo.InvariantCulture), XElement)
                                collectionElement.Add(elements)
                            End If
                        Else
                            Dim element = New XElement(item.GetType.Name)
                            element.Add(item)
                            collectionElement.Add(element)
                        End If
                    Next

                    rootElement.Add(collectionElement)

                ElseIf propertyInfo.CanWrite OrElse _
                       propertyInfo.PropertyType.GetInterface("ISerializable") IsNot Nothing Then

                    If propertyInfo.PropertyType Is Me.GetType Then
                        Continue For
                    End If

                    If propertyInfo.PropertyType Is GetType(TimeSpan) Then
                        Dim element = New XElement(propertyInfo.Name)
                        element.Add(DirectCast(propertyInfo.GetValue(Me, Nothing), TimeSpan).ToString)
                        rootElement.Add(element)

                    ElseIf Not propertyInfo.PropertyType.IsClass OrElse _
                       propertyInfo.PropertyType Is GetType(String) Then
                        Dim element = New XElement(propertyInfo.Name)
                        element.Add(propertyInfo.GetValue(Me, Nothing))
                        rootElement.Add(element)

                    ElseIf propertyInfo.PropertyType Is GetType(Uri) Then
                        Dim element = New XElement(propertyInfo.Name)
                        element.Add(propertyInfo.GetValue(Me, Nothing))
                        rootElement.Add(element)
                    Else
                        Dim itemType = propertyInfo.PropertyType
                        If itemType.BaseType.GetInterface("ISerializable") IsNot Nothing Then
                            Dim elements = DirectCast(itemType.InvokeMember("Serialize", BindingFlags.InvokeMethod, Nothing, propertyInfo.GetValue(Me, Nothing), New Object() {propertyInfo.Name}, System.Globalization.CultureInfo.InvariantCulture), XElement)
                            rootElement.Add(elements)
                        End If
                    End If
                End If
            Next

            Return rootElement
        End Function

        Public Overridable Sub Deserialize(ByVal rootElement As System.Xml.Linq.XElement) Implements ISerializable.Deserialize
            For Each propertyInfo In Me.GetType.GetProperties()

                If Not propertyInfo.CanWrite AndAlso _
                   propertyInfo.PropertyType.IsGenericType AndAlso _
                   propertyInfo.PropertyType.FullName.StartsWith("System.Collections.ObjectModel.ObservableCollection", StringComparison.Ordinal) Then
                    ' ReadOnly ObservableCollection Property

                    Dim list = DirectCast(propertyInfo.GetValue(Me, Nothing), IList)

                    Dim argType = propertyInfo.PropertyType.GetGenericArguments.First
                    Dim isDeserializable = False
                    If argType.GetInterface("ISerializable") IsNot Nothing Then
                        isDeserializable = True
                    End If

                    Dim listName = propertyInfo.Name
                    Dim listElements = From el In rootElement.Elements Where el.Name = listName

                    If listElements.Count = 0 Then
                        Exit For
                    End If

                    Dim itemName = argType.Name
                    Dim elements = From el In listElements.First.Elements Where el.Name = itemName

                    If isDeserializable Then
                        For Each el In elements
                            Dim deserializableClass = Activator.CreateInstance(argType)
                            argType.InvokeMember("Deserialize", BindingFlags.InvokeMethod, Nothing, deserializableClass, New Object() {el}, System.Globalization.CultureInfo.InvariantCulture)
                            list.Add(deserializableClass)
                        Next
                    Else
                        For Each el In elements
                            If argType Is GetType(String) Then
                                list.Add(el.Value)
                            End If
                        Next
                    End If

                ElseIf propertyInfo.CanWrite OrElse _
                       propertyInfo.PropertyType.GetInterface("ISerializable") IsNot Nothing Then

                    If Not propertyInfo.PropertyType.IsClass OrElse _
                       propertyInfo.PropertyType Is GetType(String) OrElse _
                       propertyInfo.PropertyType Is GetType(Uri) OrElse _
                       propertyInfo.PropertyType Is GetType(TimeSpan) Then

                        Dim name = propertyInfo.Name
                        Dim elements = From el In rootElement.Elements Where el.Name = name
                        If elements.Count = 1 Then
                            Dim obj = Convert(elements.Value, propertyInfo.PropertyType)
                            propertyInfo.SetValue(Me, obj, BindingFlags.SetProperty, Nothing, Nothing, System.Globalization.CultureInfo.InvariantCulture)
                        End If


                    Else

                        Dim name = propertyInfo.Name
                        Dim elements = From el In rootElement.Elements Where el.Name = name

                        If elements.Count > 0 Then
                            If propertyInfo.CanWrite Then
                                Dim deserializableClass = Activator.CreateInstance(propertyInfo.PropertyType)
                                propertyInfo.PropertyType.InvokeMember("Deserialize", BindingFlags.InvokeMethod, Nothing, deserializableClass, New Object() {elements.First}, System.Globalization.CultureInfo.InvariantCulture)
                                propertyInfo.SetValue(Me, deserializableClass, BindingFlags.SetProperty, Nothing, Nothing, System.Globalization.CultureInfo.InvariantCulture)
                            Else
                                Dim propertyClass = propertyInfo.ReflectedType.InvokeMember(name, BindingFlags.GetProperty, Nothing, Me, Nothing)
                                propertyInfo.PropertyType.InvokeMember("Deserialize", BindingFlags.InvokeMethod, Nothing, propertyClass, New Object() {elements.First}, System.Globalization.CultureInfo.InvariantCulture)
                            End If
                        End If

                    End If
                End If
            Next
        End Sub

        Protected Shared Function Convert(ByVal value As String, ByVal type As Type) As Object
            Dim obj As Object = Nothing

            Try
                If type Is GetType(Boolean) Then
                    obj = CBool(value)
                ElseIf type Is GetType(DateTime) Then
                    obj = CDate(value)
                ElseIf type Is GetType(String) Then
                    obj = value
                ElseIf type Is GetType(Integer) Then
                    obj = CInt(value)
                ElseIf type Is GetType(Double) Then
                    obj = CDbl(value)
                ElseIf type Is GetType(Uri) Then
                    If Uri.IsWellFormedUriString(value, UriKind.RelativeOrAbsolute) Then
                        obj = New Uri(value)
                    End If
                ElseIf type Is GetType(TimeSpan) Then
                    Dim span As TimeSpan
                    If TimeSpan.TryParse(value, span) Then
                        obj = span
                    End If
                ElseIf type.IsSubclassOf(GetType(System.Enum)) Then
                    obj = [Enum].Parse(type, value)
                End If

            Catch argEx As ArgumentException
                ' Parse Exception
            Catch invalidCastEx As InvalidCastException

            End Try

            Return obj
        End Function

#End Region

    End Class

End Namespace